package bu.met.cs664.cox;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * Maintains game board state for a single game of Mancala
 *
 * @author mcox
 */
public class Board {

    /** Enumeration of players */
    public static enum Player {
        PLAYER_ONE,
        PLAYER_TWO;

        /** Return the other player */
        public static Player complement(Player player) {
            if (player == PLAYER_ONE) {
                return PLAYER_TWO;
            }

            return PLAYER_ONE;
        }
    }

    /** Resulting game state from the current turn */
    public static enum BoardState {
        NEXT_PLAYER,
        SAME_PLAYER,
        PLAYER_ONE_WINS,
        PLAYER_TWO_WINS,
        DRAW
    }

    /** The resulting stones captured and game state as the result of the current turn */
    public static class TurnResult {

        /** The resulting state of the board after taking the turn */
        private final BoardState state;

        /** The stones captured by the current player during this turn */
        private final int stonesCaptured;

        /** The stones captured by the opponent during the turn */
        private final int stonesOpponentCaptured;

        /** Count of stones in player one's store after the turn */
        private final int playerOneStore;

        /** Count of stones in player two's store after the turn */
        private final int playerTwoStore;

        /** Construct a new turn result with board state and stones captured by the player */
        public TurnResult(BoardState state, int stonesCaptured, int stonesOpponentCaptured,
                          int playerOneStore, int playerTwoStore) {
            this.state = state;
            this.stonesCaptured = stonesCaptured;
            this.stonesOpponentCaptured = stonesOpponentCaptured;
            this.playerOneStore = playerOneStore;
            this.playerTwoStore = playerTwoStore;
        }

        /** Returns true if the game state is a terminal state */
        public boolean isGameOver() {
            return state == BoardState.DRAW || state == BoardState.PLAYER_ONE_WINS || state == BoardState.PLAYER_TWO_WINS;
        }

        /** Resulting state of the turn */
        public BoardState getState() {
            return state;
        }

        /** Resulting stones captured by the player */
        public int getStonesCaptured() {
            return stonesCaptured;
        }

        /** Resulting stones captured by the opponent */
        public int getStonesOpponentCaptured() {
            return stonesOpponentCaptured;
        }

        /** Count of stones in player one's store */
        public int getPlayerOneStore() {
            return playerOneStore;
        }

        /** Count of stones in player two's store */
        public int getPlayerTwoStore() {
            return playerTwoStore;
        }
    }

    /** Total pits on the board */
    private final static int MOD_BOARD_SIZE = 14;

    /** Total pits on a side */
    private final static int MOD_PLAYER_SIZE = 7;

    /** Opponent pit */
    private final static int MOD_OPPONENT_STORE = 13;

    /** Index for each player's store */
    private final static int STORE_INDEX = 6;

    /** Pits for player one and store ([6] is store) */
    private int [] playerOnePits = new int[7];

    /** Pits for player two and store ([6] is store) */
    private int [] playerTwoPits = new int[7];

    /** Map a player enum to pits */
    private final Map<Player, int[]> playerToPits = new HashMap<>();

    /** Initialize a new game board */
    public Board() {
        Arrays.fill(playerOnePits, 4);
        Arrays.fill(playerTwoPits, 4);
        playerOnePits[6] = 0;
        playerTwoPits[6] = 0;

        playerToPits.put(Player.PLAYER_ONE, playerOnePits);
        playerToPits.put(Player.PLAYER_TWO, playerTwoPits);
    }

    /** Initialize a new game board with preset board layout */
    public Board(Board base) {
        this.playerOnePits = base.playerOnePits.clone();
        this.playerTwoPits = base.playerTwoPits.clone();

        playerToPits.put(Player.PLAYER_ONE, playerOnePits);
        playerToPits.put(Player.PLAYER_TWO, playerTwoPits);
    }

    /**
     * Take a turn move rocks for a player
     * @param player The player taking their turn
     * @param pit The pit to start from
     */
    public TurnResult takeTurn(Player player, int pit) {
        if (!isValidMove(player, pit)) {
            throw new IllegalArgumentException(pit + " is not a legal move");
        }

        BoardState boardState = BoardState.NEXT_PLAYER;
        int stonesCaptured = 0;
        int stonesOpponentCaptured = 0;
        int [] playerPits = playerToPits.get(player);
        int [] opponentPits = playerToPits.get(Player.complement(player));
        int stones = playerToPits.get(player)[pit];
        playerPits[pit] = 0;

        while (stones > 0) {
            pit = (isOpponentStore(pit + 1) ? pit + 2 : pit + 1) % MOD_BOARD_SIZE;
            stones--;

            if (pit < MOD_PLAYER_SIZE) {
                playerPits[pit] += 1;
                if (pit == STORE_INDEX) {
                    ++stonesCaptured;
                }

                // Apply last placed stone rules
                if (shouldPlayAgain(stones, pit)) {
                    boardState = BoardState.SAME_PLAYER;
                    //System.out.println(String.format("%s's final deposit was into their own store, move again",
                    //        player));
                } else if (shouldCapturePit(stones, player, pit)) {
                    int oppositePit = STORE_INDEX - 1 - pit;
                    stonesCaptured += opponentPits[oppositePit] + playerPits[pit];
                    //System.out.println(String.format("Final deposit was into an empty pit capturing %d stones",
                    //        opponentPits[oppositePit] + playerPits[pit]));
                    playerPits[STORE_INDEX] += opponentPits[oppositePit] + playerPits[pit];
                    opponentPits[oppositePit] = 0;
                    playerPits[pit] = 0;
                }
            } else {
                opponentPits[pit % MOD_PLAYER_SIZE] += 1;
            }
        }

        // If one side has no more pits then the game is over
        if (!hasStones(Player.PLAYER_ONE) || !hasStones(Player.PLAYER_TWO)) {
            if (hasStones(Player.PLAYER_ONE)) {
                final int captured = capturePlayer(Player.PLAYER_ONE);

                if (player == Player.PLAYER_ONE) {
                    stonesCaptured += captured;
                } else {
                    stonesOpponentCaptured += captured;
                }
            } else {
                final int captured = capturePlayer(Player.PLAYER_TWO);

                if (player == Player.PLAYER_TWO) {
                    stonesCaptured += captured;
                } else {
                    stonesOpponentCaptured += captured;
                }
            }

            if (playerOnePits[STORE_INDEX] > playerTwoPits[STORE_INDEX]) {
                boardState = BoardState.PLAYER_ONE_WINS;
            } else if (playerTwoPits[STORE_INDEX] > playerOnePits[STORE_INDEX]) {
                boardState = BoardState.PLAYER_TWO_WINS;
            } else {
                boardState = BoardState.DRAW;
            }
        }

        return new TurnResult(boardState, stonesCaptured, stonesOpponentCaptured,
                playerPits[STORE_INDEX], opponentPits[STORE_INDEX]);
    }

    /** Is this a valid move for the current player */
    public boolean isValidMove(Player player, int pit) {
       return pit >= 0 && pit <= 5 && playerToPits.get(player)[pit] != 0;
    }

    /** Is the current pit the opponent's pit? */
    private boolean isOpponentStore(int pit) {
        return pit % MOD_OPPONENT_STORE == 0;
    }

    /** Are there any stones in the player's pits (not including store) */
    private boolean hasStones(Player player) {
        int [] pits = playerToPits.get(player);
        int count = 0;
        for (int i = 0; i < STORE_INDEX; ++i) {
            count += pits[i];
        }

        return count != 0;
    }

    /** If the last deposit is in the player's store then they go again */
    private boolean shouldPlayAgain(int stonesLeft, int pit) {
        return stonesLeft == 0 && pit == STORE_INDEX;
    }

    /** If the last deposit is into the player's own pit with zero stones then we should capture the opposite pit */
    private boolean shouldCapturePit(int stonesLeft, Player player, int pit) {
        return stonesLeft == 0 && playerToPits.get(player)[pit] == 1;
    }

    /** Capture all stones in play for a player */
    private int capturePlayer(Player player) {
        int captured = 0;
        int [] pits = playerToPits.get(player);
        for (int i = 0; i < STORE_INDEX; ++i) {
            pits[STORE_INDEX] += pits[i];
            captured += pits[i];
            pits[i] = 0;
        }

        return captured;
    }

    /** Return the players current store count */
    public int getPlayerStoreCount(Player player) {
        return playerToPits.get(player)[STORE_INDEX];
    }

    /** Render board with stone counts. Renders clockwise from left, top, right, bottom */
    public String renderBoard(String view, int s1, int p1, int p2, int p3, int p4, int p5, int p6,
                              int s2, int p7, int p8, int p9, int p10, int p11, int p12) {
        return String.format(
                " ___________________________ %s ____________________________\n" +
                "/  _____     _____    _____    _____    _____    _____    _____          \\\n" +
                "/ |     |   [_%02d__]  [_%02d__]  [_%02d__]  [_%02d__]  [_%02d__]  [_%02d__]  _____  \\\n" +
                "/ | %02d  |                                                        |     | \\\n" +
                "/ |_____|   _____    _____    _____    _____    _____    _____   | %02d  | \\\n" +
                "/          [_%02d__]  [_%02d__]  [_%02d__]  [_%02d__]  [_%02d__]  [_%02d__]  |_____| \\\n" +
                "/________________________________________________________________________\\\n",
                view, p1, p2, p3, p4, p5, p6, s1,
                s2, p7, p8, p9, p10, p11, p12);
    }

    /** Render with player one on bottom */
    public String renderForP1() {
        return renderBoard("Player One View", playerTwoPits[6], playerTwoPits[5], playerTwoPits[4], playerTwoPits[3],
                playerTwoPits[2], playerTwoPits[1], playerTwoPits[0], playerOnePits[6],  playerOnePits[0],
                playerOnePits[1], playerOnePits[2], playerOnePits[3], playerOnePits[4], playerOnePits[5]);
    }

    /** Render with player two on bottom */
    public String renderForP2() {
        return renderBoard("Player Two View", playerOnePits[6],  playerOnePits[5], playerOnePits[4], playerOnePits[3],
                playerOnePits[2], playerOnePits[1], playerOnePits[0], playerTwoPits[6], playerTwoPits[0],
                playerTwoPits[1], playerTwoPits[2], playerTwoPits[3], playerTwoPits[4], playerTwoPits[5]);
    }
}
